package godebug

import (
	"bufio"
	"fmt"
	"os"
	"reflect"
	"strings"
	"sync/atomic"
	"unicode"
	"unsafe"

	"github.com/mailgun/godebug/Godeps/_workspace/src/github.com/0xfaded/eval"
)

var buildMode = "" // overwritten by -ldflags in tests

const (
	run int32 = iota
	next
	step
)

type contextManager interface {
	GetValue(key interface{}) (value interface{}, ok bool)
	// SetValues is not intended to be called multiple times in the same stack,
	// even though github.com/jtolds/gls supports it.
	SetValues(contextCall func(), keyVal ...interface{})
}

var (
	currentState     int32
	currentDepth     int
	debuggerDepth    int
	justLeft         bool // we returned from a function we were stepping through and have not yet run any debug code in the parent function
	context          = getContextManager()
	goroutineKey     = 0
	currentGoroutine uint32
	ids              idPool
)

// EnterFunc marks the beginning of a function. Calling fn should be equivalent to running
// the function that is being entered. If proceed is false, EnterFunc did in fact call
// fn, and so the caller of EnterFunc should return immediately rather than proceed to
// duplicate the effects of fn.
func EnterFunc(fn func()) (ctx *Context, proceed bool) {
	// We've entered a new function. If we're in step or next mode we have some bookkeeping to do,
	// but only if the current goroutine is the one the debugger is following.
	//
	// We consult context to determine whether we are the goroutine the debugger is following. If
	// context has not seen our goroutine before, the ok it returns is false. Why would that happen?
	// godebug supports generating debug code for a library that is later built into a binary. If that
	// happens, then context will not see any goroutines until they call code from the debugged library.
	// context will also not see any goroutines while the debugger is in state run.
	val, ok := context.GetValue(goroutineKey)
	if !ok {
		// This is the first time context has seen the current goroutine.
		//
		// Or, more accurately and precisely: This is the first frame in the current stack that contains
		// code that has been generated by godebug.
		//
		// We record some bookkeeping information with context and then continue running. This means we will
		// invoke fn, which means the caller should not proceed. After running it, return false.
		id := uint32(ids.Acquire())
		defer ids.Release(uint(id))
		context.SetValues(fn, goroutineKey, id)
		return nil, false
	}
	if val.(uint32) == atomic.LoadUint32(&currentGoroutine) && currentState != run {
		if justLeft {
			// This means this goroutine ran ExitFunc followed by EnterFunc with no intervening debug calls,
			// probably because the parent caller is in another package which has not been instrumented.
			debuggerDepth++
			justLeft = false
		}
		currentDepth++
	}
	return &Context{goroutine: val.(uint32)}, true
}

// EnterFuncLit is like EnterFunc, but intended for function literals. The passed callback takes a *Context rather than no input.
func EnterFuncLit(fn func(*Context)) (ctx *Context, proceed bool) {
	val, ok := context.GetValue(goroutineKey)
	if !ok {
		id := uint32(ids.Acquire())
		defer ids.Release(uint(id))
		context.SetValues(func() {
			fn(&Context{goroutine: id})
		}, goroutineKey, id)
		return nil, false
	}
	if val.(uint32) == atomic.LoadUint32(&currentGoroutine) && currentState != run {
		if justLeft {
			// This means this goroutine ran ExitFunc followed by EnterFuncLit with no intervening debug calls,
			// probably because the parent caller is in another package which has not been instrumented.
			debuggerDepth++
			justLeft = false
		}
		currentDepth++
	}
	return &Context{goroutine: val.(uint32)}, true
}

// EnterFuncWithRecovers is a special wrapper for functions that call recover().
// recover only works if it is called directly by a deferred function. It does not work if
// a deferred function calls a function that in turn calls another function that calls recover.
// But EnterFunc and EnterFuncLit depend on being able to wrap their functions and call them
// farther down in the stack.
//
// The solution is to create a new goroutine, use EnterFuncLit as normal in the new goroutine,
// and have the code generator replace calls to recover with channel communications. Then in
// the original function frame, we call recover when asked to and pass its value into the
// new goroutine over a channel.
//
// EnterFuncWithRecovers takes care of maintaining goroutine-local-storage in the new
// goroutine, as well as propagating any panic from that goroutine to the original goroutine.
func EnterFuncWithRecovers(r chan chan interface{}, fn func(*Context)) (<-chan chan interface{}, chan interface{}) {
	var (
		quit      = make(chan struct{})
		recovers  = make(chan chan interface{})
		rr        = make(chan interface{})
		panicChan = make(chan interface{})
		ctx       *Context
		ok        bool
	)
	go func() {
		for {
			select {
			case <-quit:
				return
			case r <- rr: // send the read end to the embedded function
				recovers <- rr // send the write end to the calling function
			}
		}
	}()
	Go(func() {
		didPanic := true
		defer func() {
			close(quit)
			close(recovers)
			if didPanic {
				panicChan <- recover()
			}
			close(panicChan)
		}()
		if ctx, ok = EnterFuncLit(fn); ok {
			defer ExitFunc(ctx)
			fn(ctx)
		}
		didPanic = false
	})
	return recovers, panicChan
}

// ExitFunc marks the end of a function.
func ExitFunc(ctx *Context) {
	if atomic.LoadUint32(&currentGoroutine) != ctx.goroutine {
		return
	}
	if currentState == run {
		return
	}
	if currentState == next && currentDepth == debuggerDepth {
		debuggerDepth--
		justLeft = true
	}
	currentDepth--
}

// Context contains debugging context information.
type Context struct {
	goroutine uint32
}

type caseSentinel int

// Case marks a case clause. Intended to be inserted as its own case clause
// immediately prior to the case clause it is marking.
func Case(c *Context, s *Scope, line int) interface{} {
	Line(c, s, line)
	return caseSentinel(0)
}

// Comm marks a case in a select statement.
// It returns a nil channel to read from as a new case immediately before the case it is marking.
func Comm(c *Context, s *Scope, line int) chan struct{} {
	Line(c, s, line)
	return nil
}

// EndSelect marks the end of a select statement.
// It returns a nil channel to read from as the last case of that select statement.
func EndSelect(c *Context, s *Scope) chan struct{} {
	if shouldPause(c) {
		fmt.Println("< All channel expressions evaluated. Choosing case to proceed. >")
	}
	return nil
}

// Select marks a select statement.
func Select(c *Context, s *Scope, line int) {
	if !shouldPause(c) {
		return
	}
	Line(c, s, line)
	// Assumes the debugger hasn't switched goroutines. Valid assumption now,
	// will probably change in the future.
	if currentState != run {
		fmt.Println("< Evaluating channel expressions and RHS of send expressions. >")
	}
}

// Line marks a normal line where the debugger might pause.
func Line(c *Context, s *Scope, line int) {
	lineWithPrefix(c, s, line, "")
}

func shouldPause(c *Context) bool {
	return atomic.LoadUint32(&currentGoroutine) == c.goroutine &&
		(currentState == step || (currentState == next && currentDepth == debuggerDepth))
}

func lineWithPrefix(c *Context, s *Scope, line int, prefix string) {
	if !shouldPause(c) {
		return
	}
	debuggerDepth = currentDepth
	justLeft = false
	fmt.Println("-> " + prefix + strings.TrimSpace(s.fileText[line-1])) // token.Position.Line starts at 1.
	waitForInput(s, line)
}

var skipNextElseIfExpr bool

// ElseIfSimpleStmt marks a simple statement preceding an "else if" expression.
func ElseIfSimpleStmt(c *Context, s *Scope, line int) {
	Line(c, s, line)
	if currentState == next {
		skipNextElseIfExpr = true
	}
}

// ElseIfExpr marks an "else if" expression.
func ElseIfExpr(c *Context, s *Scope, line int) {
	if atomic.LoadUint32(&currentGoroutine) != c.goroutine {
		return
	}
	if skipNextElseIfExpr {
		skipNextElseIfExpr = false
		return
	}
	Line(c, s, line)
}

// Defer marks a defer statement. Intended to be run in a defer statement of its own
// after the corresponding defer in the original source.
func Defer(c *Context, s *Scope, line int) {
	lineWithPrefix(c, s, line, "<Running deferred function>: ")
}

// SetTrace is deprecated. It will be deleted in a future release.
func SetTrace() {
}

// SetTraceGen is the generated entrypoint to the debugger.
func SetTraceGen(ctx *Context) {
	// TODO: The case where the user calls SetTrace multiple times has not been thought out at all yet.
	if atomic.LoadInt32(&currentState) != run {
		return
	}
	atomic.StoreUint32(&currentGoroutine, ctx.goroutine)
	currentState = step
}

var help = `
Commands:
    (h) help: Print this help.
    (n) next: Run the next line.
    (s) step: Run for one step.
    (c) continue: Run until the next breakpoint.
    (l) list: Show the current line in context of the code around it.
    (p) print <expression>: Print a variable or any other Go expression.
    (q) quit: Exit the program. Uses os.Exit; deferred functions are not run.

Commands may be given by their full name or by their parenthesized abbreviation.

Pressing enter without typing anything repeats the previous command.
`

var prevCommand string

func waitForInput(scope *Scope, line int) {
	for {
		s, ok := promptUser()
		if !ok {
			fmt.Println("quitting session")
			currentState = run
			return
		}
		s = strings.TrimSpace(s)
		if s == "" {
			s = prevCommand
		} else {
			prevCommand = s
		}
		switch s {
		case "":
		case "?", "h", "help":
			fmt.Println(help)
			continue
		case "n", "next":
			currentState = next
			return
		case "s", "step":
			currentState = step
			return
		case "c", "continue":
			currentState = run
			return
		case "l", "list":
			printContext(scope.fileText, line, 4)
			continue
		case "q", "quit":
			os.Exit(0)
		}
		fields := strings.Fields(s)
		if len(fields) > 0 && (fields[0] == "p" || fields[0] == "print") {
			if len(fields) > 1 {
				results, panik, compileErrs := goEval(strings.Join(fields[1:], " "), scope)
				switch {
				case compileErrs != nil:
					for _, err := range compileErrs {
						fmt.Println(err)
					}
				case panik != nil:
					fmt.Printf("panic (recovered): %v\n", panik)
				default:
					s := make([]string, len(results))
					for i, r := range results {
						if !r.CanInterface() {
							if r.CanAddr() {
								r = reflect.NewAt(r.Type(), unsafe.Pointer(r.UnsafeAddr())).Elem()
							} else {
								s[i] = fmt.Sprintf("godebug cannot access this field or method. Sorry! Let us know about it at github.com/mailgun/godebug/issues/new and we'll fix it")
							}
						}
						ifc := r.Interface()
						if _, ok := ifc.(*eval.ConstNumber); ok {
							s[i] = fmt.Sprintf("%v", ifc)
						} else {
							s[i] = fmt.Sprintf("%#v", ifc)
						}
					}
					fmt.Println(strings.Join(s, ", "))
				}
			} else {
				fmt.Println("usage: print <expression>")
			}
			continue
		}
		fmt.Println(`Invalid command. Try "help".`)
		if _, ok := scope.getIdent(strings.TrimSpace(s)); ok {
			fmt.Printf("If you want to print the variable %s, use the print command.\n", strings.TrimSpace(s))
		}
	}
}

// goEval runs eval.EvalEnv in a new goroutine. This is a quick hack to
// keep the debugger from pausing while running eval.EvalEnv.
// It is also used to recover from panics in the eval library.
func goEval(expr string, env eval.Env) (result []reflect.Value, panik error, compileErrors []error) {
	c := make(chan struct{})
	go func() {
		defer func() {
			if r := recover(); r != nil {
				panik = fmt.Errorf("%v", r)
			}
			close(c)
		}()
		result, panik, compileErrors = eval.EvalEnv(expr, env)
	}()
	<-c
	return
}

func dereference(i interface{}) interface{} {
	return reflect.ValueOf(i).Elem().Interface()
}

func printContext(lines []string, line, contextCount int) {
	line-- // token.Position.Line starts at 1.
	fmt.Println()
	for i := line - contextCount; i <= line+contextCount; i++ {
		prefix := "    "
		if i == line {
			prefix = "--> "
		}
		if i >= 0 && i < len(lines) {
			line := strings.TrimRightFunc(prefix+lines[i], unicode.IsSpace)
			fmt.Println(line)
		}
	}
	fmt.Println()
}

var input = bufio.NewScanner(os.Stdin)

func fallbackPrompt() (response string, ok bool) {
	fmt.Print("(godebug) ")
	if !input.Scan() {
		return "", false
	}
	return input.Text(), true
}

// This gets overridden when running in a browser or in a terminal supported
// by our readline package.
var promptUser = fallbackPrompt
